<template>
    <div
        :style="{ zIndex: zIndex, height: realHeight }"
        :class="{ 'full-screen': isFullScreen }"
        class="editor-edit"
    >
        <div class="editor-wrap">
            <!-- 菜单栏 -->
            <div class="editor-menu">
                <div class="menu-group-left">
                    <i
                        v-if="uploadOpt.url"
                        class="wmui icon-image"
                        title="上传图片 Ctrl+g"
                        hotkey="Ctrl+g"
                        @click="uploadClick"
                    >
                        <input
                            v-show="false"
                            ref="upload"
                            :name="uploadOpt.name"
                            :accept="uploadOpt.accept"
                            type="file"
                            @change="fileUpload"
                        />
                    </i>
                    <i
                        :class="{ disable: !canUndo }"
                        class="wmui icon-undo"
                        title="撤销 Ctrl+z"
                        hotkey="Ctrl+z"
                        @click="undo"
                    />
                    <i
                        :class="{ disable: !canRedo }"
                        class="wmui icon-redo"
                        title="恢复 Ctrl+y"
                        hotkey="Ctrl+y"
                        @click="redo"
                    />
                    <i
                        class="wmui icon-preview"
                        title="预览"
                        @click="doPreview"
                    />
                    <i
                        class="wmui icon-save"
                        title="保存 Ctrl+s"
                        hotkey="Ctrl+s"
                        @click="doSave"
                    />
                </div>
                <div class="menu-group-right">
                    <i
                        class="wmui icon-fullscreen"
                        title="全屏"
                        @click="isFullScreen = !isFullScreen"
                    />
                </div>
            </div>
            <!-- 内容区 -->
            <div class="editor-content">
                <div class="content-wrap">
                    <!-- 编辑区 -->
                    <textarea
                        v-show="showContent"
                        ref="editor"
                        v-model="content"
                        class="content-editor"
                        autocapitalize="off"
                        @scroll="scrollReset"
                        @keydown="keydown"
                        @paste="pasteEvent"
                    />
                    <!-- 预览区 -->
                    <editor-preview
                        v-show="showPreview"
                        ref="preview"
                        :content="content"
                        :options="options"
                    />
                </div>
                <transition name="wmui-animation-fade">
                    <div
                        v-show="statusMessage.show"
                        :class="statusMessage.type"
                        class="upload-status"
                    >
                        {{ statusMessage.text }}
                    </div>
                </transition>
            </div>
        </div>
    </div>
</template>
<script>
import Vue from "vue";
import EditorPreview from "./editor-preview.vue";

const isServer = Vue.prototype.$isServer || process.server;
// 选中内容的位置
function getEditorSelection(editor) {
    return {
        start: editor.selectionStart,
        end: editor.selectionEnd
    };
}
// 设置选中区域
function setEditorRange(editor, start, length = 0) {
    editor.setSelectionRange(start, start + length);
    editor.focus();
}
export default {
    name: "EditorEdit",
    components: {
        EditorPreview
    },
    props: {
        value: {
            type: String,
            default: ""
        },
        options: {
            type: Object,
            default: () => ({})
        },
        upload: {
            type: Object,
            default: () => ({})
        },
        zIndex: {
            type: Number,
            default: 999
        },
        height: {
            type: String,
            default: "70vh"
        },
        preview: {
            type: Boolean,
            default: true
        },
        maxSize: {
            type: Number,
            default: 300 * 1024 // 当图片大大于300kb，自动压缩
        }
    },
    data() {
        return {
            content: "",
            html: "",
            history: [],
            uploadOpt: {
                name: "file",
                accept: "*", // image/jpg,image/jpeg,image/png,image/gif
                url: false,
                headers: {}
            },
            isFullScreen: false,
            currentIndex: 0,
            currentTimeout: null,
            showContent: true,
            showPreview: true,
            statusMessage: {
                type: "",
                text: "",
                timeout: 0,
                show: false
            }
        };
    },
    computed: {
        realHeight() {
            return this.isFullScreen ? "100%" : this.height;
        },
        // ctrl+z
        canUndo() {
            return this.currentIndex > 0;
        },
        // ctrl+y
        canRedo() {
            return this.currentIndex < this.history.length - 1;
        }
    },
    watch: {
        value(value) {
            if (this.content !== value) this.content = value;
        },
        content() {
            if (isServer) return;
            this.$emit("input", this.content);
            this.scrollReset();
            if (this.content === this.history[this.currentIndex]) return;
            // 内容变化，重新保存到历史记录，每隔500毫秒添加一条历史记录
            window.clearTimeout(this.currentTimeout);
            this.currentTimeout = setTimeout(() => {
                this.saveHistory();
                this.$emit("save-history");
            }, 500);
        },
        // 执行了撤销或恢复，currentIndex会变化，重置内容
        currentIndex() {
            const history = this.history[this.currentIndex];
            this.content = history;
        }
    },

    destroyed() {},
    mounted() {
        this.uploadOpt = { ...this.uploadOpt, ...this.upload };
        // this.value接收v-model中的值
        this.content = this.value;
        // 保存一条历史记录
        this.history.push(this.content);
        this.showPreview = this.preview;
    },
    methods: {
        pasteEvent(event) {
            if (isServer) return;
            const { items } =
                event.clipboardData || event.originalEvent.clipboardData;
            for (const key in items) {
                if ({}.hasOwnProperty.call(items, key)) {
                    const item = items[key];
                    // 如果是图片
                    if (item.kind === "file") {
                        const file = item.getAsFile();
                        this.fileParam(file);
                    }
                }
            }
        },
        doPreview() {
            if (!this.preview) {
                this.showContent = !this.showContent;
                this.showPreview = !this.showPreview;
            } else {
                this.showContent = !this.showContent;
            }
        },
        doSave() {
            this.$emit("save", true);
        },
        scrollReset() {
            if (isServer) return;
            const { editor } = this.$refs;
            const scrollHeight =
                editor.scrollHeight - editor.clientHeight ||
                editor.scrollHeight;
            // 获得被卷去的比例
            const scroll = editor.scrollTop / scrollHeight;
            const preview = this.$refs.preview.$el;
            // 设置预览区被卷去的头部
            const preTop =(preview.scrollHeight - preview.clientHeight) * scroll;
            preview.scrollTop = preTop;
        },
        // 快捷键配置
        keydown(e) {
            const code = e.key;
            if (e.ctrlKey === true || e.Meta) {
                const hotkey = `Ctrl+${code}`;
                const el = this.$el.querySelector(`[hotkey='${hotkey}']`);
                if (el) {
                    e.preventDefault();
                    el.click();
                }
            } else if (code === "Tab") {
                e.preventDefault();
                const TAB_SPACE = "  ";
                const { editor } = this.$refs;
                const { start, end } = getEditorSelection(editor);
                const { after } = this.selectedStr(start, end);
                let { before, select } = this.selectedStr(start, end);
                if (select.indexOf("\n") > -1) {
                    // 一共10行，选中了5-6行中某段，beforeLR表示1-4行
                    const beforeLR = before.substr(
                        0,
                        before.lastIndexOf("\n") + 1
                    );
                    // afterLR表示5行前未选中部分
                    const afterLR = before.substr(beforeLR.length);
                    before = beforeLR;
                    // 5行开头到选中文本末尾
                    select = afterLR + select;
                    let lines = select.split("\n");
                    if (e.shiftKey) {
                        lines = lines.map(
                            l =>
                                l.substr(0, TAB_SPACE.length).replace(/(^\s*)/g, "") +
                                l.substr(TAB_SPACE.length)
                        );
                    } else {
                        lines = lines.map(l => TAB_SPACE + l);
                    }
                    const newInsert = lines.join("\n");
                    this.content = before + newInsert + after;
                    this.$nextTick(() => {
                        setEditorRange(editor, before.length, newInsert.length);
                    });
                } else {
                    // 单行缩进
                    this.insertTo(TAB_SPACE, start);
                }
            }
        },

        insertTo(text, position = getEditorSelection(this.$refs.editor).start) {
            const before = this.content.substr(0, position);
            const after = this.content.substr(position);
            this.content = before + text + after;
            this.$nextTick(() => {
                setEditorRange(this.$refs.editor, position + text.length);
            });
        },

        undo() {
            // 光标位置
            let { start } = getEditorSelection(this.$refs.editor);
            // 撤销前内容的长度
            const currentLength = this.content.length;
            // 内容重置
            if (this.canUndo) {
                this.currentIndex -= 1;
            }
            // 光标重置
            this.$nextTick(() => {
                // this.content.length是撤销后的内容长度
                // start是撤销内容后的光标位置
                start -= currentLength - this.content.length;
                setEditorRange(this.$refs.editor, start);
            });
        },
        redo() {
            // 如果可以恢复，则currentIndex++
            let { start } = getEditorSelection(this.$refs.editor);
            const currentLength = this.content.length;
            if (this.canRndo) {
                this.currentIndex += 1;
            }
            this.$nextTick(() => {
                start += this.content.length - currentLength;
                setEditorRange(this.$refs.editor, start);
            });
        },

        _status(type, text, time = (text.length / 10) * 1000) {
            if (isServer) return;
            window.clearTimeout(this.statusMessage.timeout);
            const timeout = setTimeout(() => {
                this.statusMessage.show = false;
            }, time);
            this.statusMessage = {
                type,
                text,
                timeout,
                show: true
            };
        },
        success(message, timeout) {
            this._status("success", message, timeout);
        },
        error(message, timeout) {
            this._status("error", message, timeout);
        },
        info(message, timeout) {
            this._status("info", message, timeout);
        },
        closeStatus() {
            this.statusMessage.show = false;
        },
        uploadClick() {
            this.$refs.upload.click();
        },
        fileParam(file) {
            const reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => {
                const fileType = file.type.split("/")[0];

                // 图片类型进行特殊处理
                if (fileType === "image") {
                    let img = new Image();
                    img.src = reader.result;
                    if (reader.result.length <= this.maxSize) {
                        this.uploadFormData(reader.result, file);
                        img = null;
                    } else {
                        // 进行压缩
                        img.onload = () => {
                            const data = this.compress(img);
                            this.uploadFormData(data, file);
                            img = null;
                        };
                    }
                } else {
                    this.uploadFormData(reader.result, file);
                }
            };
        },
        fileUpload() {
            if (isServer) return;
            const input = this.$refs.upload;
            const upload = this.$emit("on-upload", input);
            if (upload === false || !input.files.length) return;
            this.fileParam(input.files[0]);
        },
        compress(img) {
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            const { maxSize } = this;
            // 瓦片canvas
            const tCanvas = document.createElement("canvas");
            const tctx = tCanvas.getContext("2d");
            const initSize = img.src.length; // base64
            let { width, height } = img;

            // 如果图片大于四百万像素，计算压缩比并将大小压至400万以下
            let ratio = (width * height) / 4000000;
            if (ratio > 1) {
                ratio = Math.sqrt(ratio);
                width /= ratio;
                height /= ratio;
            } else {
                ratio = 1;
            }

            canvas.width = width;
            canvas.height = height;

            // 铺底色
            ctx.fillStyle = "#fff";
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // 如果图片像素大于100万则使用瓦片绘制
            let count = (width * height) / 1000000;
            if (count > 1) {
                count = Math.floor(Math.sqrt(count)) + 1; // 计算要分成多少块瓦片

                // 计算每块瓦片的宽和高
                const nw = width / count;
                const nh = height / count;

                tCanvas.width = nw;
                tCanvas.height = nh;

                for (let i = 0; i < count; i++) {
                    for (let j = 0; j < count; j++) {
                        tctx.drawImage(
                            img,
                            i * nw * ratio,
                            j * nh * ratio,
                            nw * ratio,
                            nh * ratio,
                            0,
                            0,
                            nw,
                            nh
                        );

                        ctx.drawImage(tCanvas, i * nw, j * nh, nw, nh);
                    }
                }
            } else {
                ctx.drawImage(img, 0, 0, width, height);
            }

            ctx.drawImage(img, 0, 0, width, height);

            // 压缩质量和图片大小没有确切的计算公式
            const ndata = canvas.toDataURL("image/jpeg", 0.6);

            tCanvas.width = 0;
            tCanvas.height = 0;
            canvas.width = 0;
            canvas.height = 0;

            return ndata;
        },
        dataURItoBlob(dataURI, file) {
            const binary = atob(dataURI.split(",")[1]);
            const array = [];
            // const fileType = dataURI.split(';')[0].replace('data:', '')

            for (let i = 0; i < binary.length; i++) {
                array.push(binary.charCodeAt(i));
            }
            return new Blob([new Uint8Array(array)], {
                type: file.type
            });
        },
        uploadFormData(baseStr, file) {
            if (isServer) return;
            if (!this.uploadOpt.url) {
                this.error("请先配置上传路径");
                return;
            }
            const blob = this.dataURItoBlob(baseStr, file);

            const formdata = new FormData();
            formdata.append(file.name, blob);
            const xhr = new window.XMLHttpRequest();
            xhr.onreadystatechange = () => {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        const success = this.$emit(
                            "upload-success",
                            xhr.responseText
                        );
                        if (success !== false) {
                            const fileType = xhr.responseText.split(".").pop();
                            const isImg = [
                                "gif",
                                "jpeg",
                                "png",
                                "tiff",
                                "jpg"
                            ].includes(fileType);
                            // 新的URL
                            isImg &&
                                this.insertTo(`\n![](${xhr.responseText})\n`);
                            !isImg &&
                                this.insertTo(
                                    `\n[文件](${xhr.responseText})\n`);
                        }
                    } else {
                        const error = this.$emit("upload-error", xhr);
                        if (error !== false) this.error("上传失败");
                    }
                }
            };
            // 上传进度条
            xhr.upload.onprogress = e => {
                const upload = this.$emit("uploading", {
                    loaded: e.loaded,
                    total: e.total
                });
                if (upload !== false) {
                    const pre = parseInt((e.loaded / e.total) * 100, 10);
                    const loaded = parseInt(e.loaded / 1024, 10);
                    const total = parseInt(e.total / 1024, 10);
                    this.info(`上传进度： ${loaded}kb / ${total}kb | ${pre}%`);
                }
            };
            // 发送请求，并设置请求头
            xhr.open("POST", this.uploadOpt.url, true);
            if (this.uploadOpt.headers) {
                Object.keys(this.uploadOpt.headers).forEach(k => {
                    xhr.setRequestHeader(k, this.uploadOpt.headers[k]);
                });
            }
            xhr.send(formdata);
        },

        // 内容发生变化，执行
        saveHistory() {
            // 第一次变化 this.history中还是原来的一项,
            this.history.splice(this.currentIndex + 1, this.history.length);
            // 变化后的内容添加进去，此时有两项
            this.history.push(this.content);
            // currentIndex表示当前内容在history中的下标
            this.currentIndex = this.history.length - 1;
        },
        // 获取选中的文本
        getSelectStr() {
            const { editor } = this.$refs;
            const { start, end } = getEditorSelection(editor);
            const { select } = this.selectedStr(start, end);
            return select;
        },
        selectedStr(start, end) {
            // 选中文本前的文本
            const before = this.content.substr(0, start);
            // 选中的文本
            const select = this.content.substr(start, end - start);
            // 选中后的文本
            const after = this.content.substr(end, this.content.length);
            return { before, select, after };
        }
    }
};
</script>
<style>
@import url("~/assets/iconfont/iconfont.css");
.editor-edit {
    position: relative;
    width: 100%;
    height: 100%;
}

.editor-edit.full-screen {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

.editor-edit .editor-wrap {
    position: relative;
    height: 100%;
    display: flex;
    flex-direction: column;
    border: 1px solid #eee;
}

.editor-edit .editor-wrap .editor-menu {
    width: 100%;
    display: flex;
    justify-content: space-between;
    overflow-x: auto;
    font-size: 0;
    background-color: #fff;
    border-bottom: 1px solid #eee;
}

.editor-edit .editor-wrap .editor-menu .wmui {
    display: inline-block;
    padding: 10px;
    font-size: 18px;
    color: #a9a9a9;
    cursor: pointer;
}

.editor-edit .editor-wrap .editor-menu .disable {
    color: #eee;
    cursor: not-allowed;
}

.editor-edit .editor-wrap .editor-content {
    position: relative;
    flex: 1;
    background: #fff;
}

.editor-edit .editor-wrap .editor-content .content-wrap {
    position: absolute;
    width: 100%;
    height: 100%;
    display: flex;
}

.editor-edit .editor-wrap .editor-content .content-wrap .content-editor {
    flex: 1;
    position: relative;
    font-size: 14px;
    line-height: 24px;
    resize: none;
    border: none;
    background-color: #eee;
    padding: 15px;
    outline: none;
    overflow: auto;
}

.editor-edit .editor-wrap .editor-content .content-wrap .content-preview {
    flex: 1;
    position: relative;
    padding: 15px;
    overflow: auto;
    background-color: #fff;
}

.editor-edit .editor-wrap .upload-status {
    position: absolute;
    top: 0;
    padding: 8px;
    background: rgba(0, 0, 0, 0.1);
    width: 100%;
    color: #333;
}

.editor-edit .editor-wrap .upload-status.info {
    background: rgba(130, 232, 255, 0.2);
}

.editor-edit .editor-wrap .upload-status.success {
    background: rgba(101, 255, 177, 0.2);
}

.editor-edit .editor-wrap .upload-status.error {
    background: rgba(255, 101, 101, 0.2);
}
</style>
